<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>easyopen开发文档</title>
        <link rel="stylesheet" href="./static/highlight/styles/vs.css">
		<script src="./static/highlight/highlight.pack.js"></script>
		<script>hljs.initHighlightingOnLoad();</script>
        <link rel="stylesheet" type="text/css" href="./static/github2-rightpart.css" media="all">
        <link rel="stylesheet" type="text/css" href="./static/github1-contents.css">
        <link rel="stylesheet" href="./static/zTreeStyle.css" type="text/css">
        <style>
            .ztree li a.curSelectedNode {
                padding-top: 0px;
                background-color: #FFE6B0;
                color: black;
                height: 16px;
                border: 1px #FFB951 solid;
                opacity: 0.8;
            }
            .ztree{
                overflow: auto;
                height:100%;
                min-height: 200px;
                top: 0px;
            }
            .task-list{list-style-type: disc !important;}
            .container{margin: 0px !important;}
            #readme .markdown-body, #readme .plain{border:0px !important;}
        </style>
    </head>
    <body>
        <div>
            <div style="width:30%;">
                <ul id="tree" class="ztree" style="overflow: auto; position: fixed; z-index: 2147483647; border: 0px none; left: 0px; bottom: 0px;">
                <!-- 目录内容在网页另存为之后将插入到此处 -->
                </ul>
            </div>
            <div id="readme" style="width:70%;margin-left:25%;">
                <article class="markdown-body">


<!-- 请把你的html正文部分粘贴到此处，在浏览器中打开之后将会自动生成目录。如果想要将目录保留并嵌入到此文档中，只需在浏览器中“另存为->网页，全部”即可 -->
                    <div class="ui container">
                        <div id="project-title">
                            <div class="title-wrap">
                                <div class="left">
                                    <i class="icon eye"></i>
                                    文档预览:
                                    easyopen
                                </div>
                                <div class="right">
                                    Export by Gitee
                                </div>
                            </div>
                        </div>

                        <div class="ui container" id="wiki-preview-container">
                            <div id="wiki-preview">
                                <div class="ui segment">
                                    <div id="page-detail" class="markdown-body">
                                        <div class='title'>easyopen开发文档</div><div class='content'><h1><a class="anchor" id="easyopen介绍_1" href="#easyopen介绍_1"></a>easyopen介绍</h1>
                                        <p>一个简单易用的接口开放平台，平台封装了常用的参数校验、结果返回等功能，开发者只需实现业务代码即可。</p>

                                        <p>easyopen的功能类似于<a href="http://open.taobao.com/docs/api.htm?spm=a219a.7629065.0.0.6cQDnQ&amp;apiId=4">淘宝开放平台</a>，它的所有接口只提供一个url，通过参数来区分不同业务。这样做的好处是接口url管理方便了，平台管理者只需维护好接口参数即可。由于参数的数量是可知的，这样可以在很大程度上进行封装。封装完后平台开发者只需要写业务代码，其它功能可以通过配置来完成。</p>

                                        <p>得益于Java的注解功能以及Spring容器对bean的管理，我们的开放接口平台就这样产生了。</p>
                                        <h1><a class="anchor" id="结构图_1" href="#结构图_1"></a>结构图</h1>
                                        <p><img alt="easyopen结构图" src="https://gitee.com/uploads/images/2018/0117/095712_1f70de95_332975.png" title="easyopen_arc.png" /></p>

                                        <ul class="task-list">
                                            <li>服务器启动完毕时，从Spring容器中找到被@ApiService标记的业务类</li>
                                            <li>循环业务类，找到被@Api标记的方法，并保存对应的参数，method，对象信息。</li>
                                            <li>客户端请求过来时，根据name-version可定位到具体的业务类中的某个方法，然后invoke方法。</li>
                                            <li>包装结果返回。</li>
                                        </ul>
                                        <h1><a class="anchor" id="功能特点_1" href="#功能特点_1"></a>功能特点</h1>
                                        <ul class="task-list">
                                            <li>开箱即用，写完业务代码直接启动服务即可使用，无需其它配置。</li>
                                            <li>参数自动校验，支持国际化参数校验（JSR-303）。</li>
                                            <li>校验功能和结果返回功能实现各自独立，方便自定义实现或扩展。</li>
                                            <li>采用注解来定义接口，维护简单方便。</li>
                                            <li>支持i18n国际化消息返回。</li>
                                            <li>自动生成文档页面，类似swagger。</li>
                                            <li>采用数字签名进行参数验证，签名算法见：easyopen\签名算法.txt。</li>
                                            <li>采用appKey-secret形式接入平台，即需要给接入方提供一个appKey和secret。</li>
                                        </ul>
                                        <h1><a class="anchor" id="快速开始_1" href="#快速开始_1"></a>快速开始</h1>
                                        <p>eclipse下（idea原理一样）</p>

                                        <ul class="task-list">
                                            <li>下载或clone项目<a href="https://gitee.com/durcframework/easyopen.git">https://gitee.com/durcframework/easyopen.git</a> <a href="https://gitee.com/durcframework/easyopen/repository/archive/master.zip">下载zip</a></li>
                                            <li>eclipse右键import... -&gt; Exsiting Maven Projects。选择easyopen目录</li>
                                            <li>导入到eclipse后会有三个工程，等待相关jar包下载。</li>
                                            <li>全部jar下载完毕后，启动easyopen-server项目，由于是spring-boot项目，直接运行EasyopenSpringbootApplication.java即可</li>
                                            <li>在easyopen-sdk中找到SdkTest.java测试用例，运行单元测试。</li>
                                        </ul>
                                        <h2><a class="anchor" id="编写业务类_1" href="#编写业务类_1"></a>编写业务类</h2>
                                        <ul class="task-list">
                                            <li>新建一个java类名为HelloworldApi，并加上@ApiService注解</li>
                                        </ul>

                                        <p>加上@ApiService注解后这个类就具有了提供接口的能力。</p>
                                        <div class="white"><div class="highlight"><pre><span class="nd">@ApiService</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">HelloWorldApi</span> <span class="o">{</span>

<span class="o">}</span>

</pre></div></div>
                                        <ul class="task-list">
                                            <li>在类中添加一个方法</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>@Api(name = "hello")
public String helloworld() {
    return "hello world";
}
</pre></div></div>
                                        <p>这个方法很简单，就返回了一个字符串，方法被@Api标记，表示对应的接口，name是接口名。</p>

                                        <p>到此，一个业务方法就写完了，接下来是编写sdk并测试。</p>
                                        <h2><a class="anchor" id="编写sdk并测试-旧-_1" href="#编写sdk并测试-旧-_1"></a>编写SDK并测试(旧)</h2>
                                        <p>此过程在easyopen-sdk中进行。</p>

                                        <ul class="task-list">
                                            <li>新建Response响应类</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>public class HelloResp extends BaseResp&lt;String&gt; {

}
</pre></div></div>
                                        <ul class="task-list">
                                            <li>新建Request请求类</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>public class HelloReq extends BaseNoParamReq&lt;HelloResp&gt; {

    public HelloReq(String name) {
        super(name);
    }


}
</pre></div></div>
                                        <p>BaseResp的泛型参数指定返回体类型，这里指定String</p>

                                        <ul class="task-list">
                                            <li>编写单元测试</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>public class HelloTest extends TestCase {

    String url = "http://localhost:8080/api";
    String appKey = "test";
    String secret = "123456";
    // 创建一个实例即可
    OpenClient client = new OpenClient(url, appKey, secret);

    @Test
    public void testGet() throws Exception {
        HelloReq req = new HelloReq("hello"); // hello对应@Api中的name属性，即接口名称

        HelloResp result = client.request(req); // 发送请求
        if (result.isSuccess()) {
            String resp = result.getBody();
            System.out.println(resp); // 返回hello world
        } else {
            throw new RuntimeException(result.getMsg());
        }

    }

}
</pre></div></div>
                                        <p>这样，一个完整的接口就写完了。</p>
                                        <h2><a class="anchor" id="编写sdk并测试-新-_1" href="#编写sdk并测试-新-_1"></a>编写SDK并测试(新)</h2><h3><a class="anchor" id="接口封装步骤_1" href="#接口封装步骤_1"></a>接口封装步骤</h3>
                                        <p>比如获取商品信息接口</p>

                                        <ul class="task-list">
                                            <li>接口名：goods.get</li>
                                            <li>版本号：""(空字符串)</li>
                                            <li>参数：goods_name 商品名称</li>
                                            <li>返回信息</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>{
    "code":"0",
    "data":{
        "goods_name":"苹果iPhoneX",
        "id":1,
        "price":8000
    }
}
</pre></div></div>
                                        <p>针对这个接口，封装步骤如下：</p>

                                        <p>1.在<code>model</code>包下新建一个类来接收<code>data</code>部分</p>

                                        <p>字段统一使用小写字母+下划线形式，如:name,user_age</p>
                                        <div class="white"><div class="highlight"><pre>public class Goods {

    private Long id;
    private String goods_name;
    private BigDecimal price;

    省略 get set
}
</pre></div></div>
                                        <p>2.在<code>response</code>包下新建一个返回类，继承<code>BaseResponse</code></p>

                                        <p>BaseResponse中有个泛型参数，填<code>Goods</code>类，表示返回的数据对象。</p>
                                        <div class="white"><div class="highlight"><pre>public class GetGoodsResponse extends BaseResponse&lt;Goods&gt; {
}
</pre></div></div>
                                        <p>3.在<code>request</code>包下新建一个请求类，继承<code>BaseRequest</code></p>

                                        <p>BaseRequest中有个泛型参数，填<code>GetGoodsResponse</code>类，表示这个请求对应的返回类。
                                            重写<code>name()</code>方法，填接口名。</p>

                                        <p>如果要指定版本号，可重写<code>version()</code>方法，或者后续使用<code>request.setVersion(version)</code>进行设置</p>
                                        <div class="white"><div class="highlight"><pre>public class GetGoodsRequest extends BaseRequest&lt;GetGoodsResponse&gt; {
    @Override
    public String name() {
        return "goods.get";
    }
}
</pre></div></div><h3><a class="anchor" id="使用方式_1" href="#使用方式_1"></a>使用方式</h3><div class="white"><div class="highlight"><pre>// 创建请求对象
GetGoodsRequest request = new GetGoodsRequest();
// 请求参数
GoodsParam param = new GoodsParam();
param.setGoods_name("iphone6");
request.setParam(param);

// 发送请求
GetGoodsResponse response = client.execute(request);

System.out.println("--------------------");
if (response.isSuccess()) {
    // 返回结果
    Goods goods = response.getData();
    System.out.println(goods);
} else {
    System.out.println("errorMsg:" + response.getMsg());
}
System.out.println("--------------------");
</pre></div></div><h3><a class="anchor" id="使用方式2-懒人版-_1" href="#使用方式2-懒人版-_1"></a>使用方式2(懒人版)</h3>
                                        <p>如果不想添加Request,Response,Model。可以用这种方式，返回data部分是Map，后续自己处理</p>
                                        <div class="white"><div class="highlight"><pre>// 创建请求对象
CommonRequest request = new CommonRequest("goods.get");
// 请求参数
Map&lt;String, Object&gt; param = new HashMap&lt;&gt;();
param.put("goods_name", "iphone6");
request.setParam(param);

// 发送请求
CommonResponse response = client.execute(request);

System.out.println("--------------------");
if (response.isSuccess()) {
    // 返回结果
    Map&lt;String, Object&gt; goods = response.getData();
    System.out.println(goods.get("goods_name"));
} else {
    System.out.println("errorCode:" + response.getCode() + ",errorMsg:" + response.getMsg());
}
System.out.println("--------------------");
</pre></div></div><h1><a class="anchor" id="错误处理_1" href="#错误处理_1"></a>错误处理</h1>
                                        <p>easyopen对错误处理已经封装好了，最简单的做法是向上throw即可，在最顶层的Controller会做统一处理。例如：</p>
                                        <div class="white"><div class="highlight"><pre>if(StringUtils.isEmpty(param.getGoods_name())) {
    throw new ApiException("goods_name不能为null");
}
</pre></div></div>
                                        <p>或者</p>
                                        <div class="white"><div class="highlight"><pre>if(StringUtils.isEmpty(param.getGoods_name())) {
    throw new RuntimeException("goods_name不能为null");
}
</pre></div></div>
                                        <p>为了保证编码风格的一致性，推荐统一使用ApiException</p>
                                        <h2><a class="anchor" id="i18n国际化_1" href="#i18n国际化_1"></a>i18n国际化</h2>
                                        <p>easyopen支持国际化消息。通过Request对象中的getLocale()来决定具体返回那种语言，客户端通过设置Accept-Language头部来决定返回哪种语言，中文是zh，英文是en。</p>

                                        <p>easyopen通过模块化来管理国际化消息，这样做的好处结构清晰，维护方便。下面就来讲解如何设置国际化消息。</p>

                                        <p>假设我们要对商品模块进行设置，步骤如下：</p>

                                        <ul class="task-list">
                                            <li>在项目的resources下新建如下目录/i18n/isv</li>
                                            <li>在isv目录下新建goods_error_zh_CN.properties属性文件</li>
                                        </ul>

                                        <p><img alt="输入图片说明" src="https://gitee.com/uploads/images/2018/0118/142511_feacd145_332975.png" title="2.png" /></p>

                                        <p>属性文件的文件名有规律， <strong>i18n/isv/goods_error</strong> 表示模块路径， <strong>_zh_CN.properties</strong> 表示中文错误消息。如果要使用英文错误，则新建一个goods_error_en.properties即可。</p>

                                        <ul class="task-list">
                                            <li>在goods_error_zh_CN.properties中配置错误信息
                                                <code>
                                                    # 商品名字不能为空
                                                    isv.goods_error_100=\u5546\u54C1\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
                                                </code></li>
                                        </ul>

                                        <p>isv.goods_error_为固定前缀，100为错误码，这两个值后续会用到。</p>

                                        <p>接下来是把属性文件加载到国际化容器当中。</p>

                                        <ul class="task-list">
                                            <li>添加国际化配置</li>
                                        </ul>

                                        <p>easyopen的所以配置操作都在ApiConfig类里面，配置工作可以在ApiController.initApiConfig(ApiConfig apiConfig)方法中进行，因此只要重写initApiConfig(ApiConfig apiConfig)方法就行了。</p>
                                        <div class="white"><div class="highlight"><pre>@Controller
@RequestMapping(value = "/api/v1")
public class IndexController extends ApiController {

    @Override
    protected void initApiConfig(ApiConfig apiConfig) {
        // 配置国际化消息
        apiConfig.getIsvModules().add("i18n/isv/goods_error");// 模块路径
        // 配置appKey秘钥
        Map&lt;String, String&gt; appSecretStore = new HashMap&lt;String, String&gt;();
        appSecretStore.put("test", "123456");

        apiConfig.addAppSecret(appSecretStore);
    }

}
</pre></div></div>
                                        <p>添加一句apiConfig.getIsvModules().add("i18n/isv/goods_error");就行</p>

                                        <ul class="task-list">
                                            <li>新建一个interface用来定义错误</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>// 按模块来定义异常消息,团队开发可以分开进行
public interface GoodsErrors {

    String isvModule = "isv.goods_error_"; // 前缀
    // 100为前缀后面的错误码
    // 这句话即可找到isv.goods_error_100错误
    ErrorMeta NO_GOODS_NAME = new ErrorMeta(isvModule, "100", "商品名称不能为空.");
}
</pre></div></div>
                                        <p>接下来就可以使用了</p>
                                        <div class="white"><div class="highlight"><pre>if (StringUtils.isEmpty(param.getGoods_name())) {
    throw GoodsErrors.NO_GOODS_NAME.getException();
}
</pre></div></div><h3><a class="anchor" id="国际化消息传参_1" href="#国际化消息传参_1"></a>国际化消息传参</h3>
                                        <p>即代码中变量传入到properties文件中去，做法是采用{0},{1}占位符。0代表第一个参数，1表示第二个参数。</p>
                                        <div class="white"><div class="highlight"><pre># 商品名称太短，不能小于{0}个字
isv.goods_error_101=\u5546\u54C1\u540D\u79F0\u592A\u77ED\uFF0C\u4E0D\u80FD\u5C0F\u4E8E{0}\u4E2A\u5B57
</pre></div></div><div class="white"><div class="highlight"><pre>if(param.getGoods_name().length() &lt; 3) {
    throw GoodsErrors.SHORT_GOODS_NAME.getException(3); // 这里的“3”会填充到{0}中
}
</pre></div></div>
                                        <p>直接放进getException(Object... params)方法参数中，因为是可变参数，可随意放。</p>
                                        <h1><a class="anchor" id="业务参数校验_1" href="#业务参数校验_1"></a>业务参数校验</h1>
                                        <p>业务参数校验采用JSR-303方式，关于JSR-303介绍可以参考这篇博文：<a href="https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/">JSR 303 - Bean Validation 介绍及最佳实践</a></p>

                                        <p>在参数中使用注解即可，框架会自动进行验证。如下面一个添加商品接口，它的参数是GoodsParam</p>
                                        <div class="white"><div class="highlight"><pre>@Api(name = "goods.add")
public void addGoods(GoodsParam param) {
    ...
}
</pre></div></div>
                                        <p>在GoodsParam中添加JSR-303注解:</p>
                                        <div class="white"><div class="highlight"><pre>public class GoodsParam {
    @NotEmpty(message = "商品名称不能为空")
    private String goods_name;
    // 省略get，set
}
</pre></div></div>
                                        <p>如果不传商品名称则返回</p>
                                        <div class="white"><div class="highlight"><pre>{"code":"100","msg":"商品名称不能为空"}
</pre></div></div><h2><a class="anchor" id="参数校验国际化_1" href="#参数校验国际化_1"></a>参数校验国际化</h2>
                                        <p>国际化的配置方式如下：</p>
                                        <div class="white"><div class="highlight"><pre>@NotEmpty(message = "{goods.name.notNull}")
private String goods_name;
</pre></div></div>
                                        <p>国际化资源文件goods_error_en.properties中添加：</p>
                                        <div class="white"><div class="highlight"><pre>goods.name.notNull=The goods_name can not be null
</pre></div></div>
                                        <p>goods_error_zh_CN.properties中添加：</p>
                                        <div class="white"><div class="highlight"><pre>goods.name.notNull=\u5546\u54C1\u540D\u79F0\u4E0D\u80FD\u4E3Anull
</pre></div></div><h2><a class="anchor" id="参数校验国际化传参_1" href="#参数校验国际化传参_1"></a>参数校验国际化传参</h2>
                                        <p>下面校验商品名称的长度，要求大于等于3且小于等于20。数字3和20要填充到国际化资源中去。</p>
                                        <div class="white"><div class="highlight"><pre>// 传参的格式：{xxx}=value1,value2...
@Length(min = 3, max = 20, message = "{goods.name.length}=3,20")
private String goods_name;
</pre></div></div>
                                        <p>goods_error_en.properties:</p>
                                        <div class="white"><div class="highlight"><pre>goods.name.length=The goods_name length must &gt;= {0} and &lt;= {1}
</pre></div></div>
                                        <p>goods_error_zh_CN.properties中添加：</p>
                                        <div class="white"><div class="highlight"><pre>goods.name.length=\u5546\u54C1\u540D\u79F0\u957F\u5EA6\u5FC5\u987B\u5927\u4E8E\u7B49\u4E8E{0}\u4E14\u5C0F\u4E8E\u7B49\u4E8E{1}

</pre></div></div>
                                        <p>这样value1，value2会分别填充到{0},{1}中</p>
                                        <h1><a class="anchor" id="自定义结果返回_1" href="#自定义结果返回_1"></a>自定义结果返回</h1><h2><a class="anchor" id="改变json输出策略_1" href="#改变json输出策略_1"></a>改变json输出策略</h2>
                                        <p>默认使用阿里的fastjson进行json输出</p>
                                        <div class="white"><div class="highlight"><pre>JSON.toJSONString(obj)
</pre></div></div>
                                        <p>如果要更换输出策略，操作方式如下：</p>
                                        <div class="white"><div class="highlight"><pre>@Override
protected void initApiConfig(ApiConfig apiConfig) {
    ...
    // 自定义json格式输出,将null字符串变成""
    apiConfig.setJsonResultSerializer(new JsonResultSerializer(){
        @Override
        public String serialize(Object obj) {
            return JSON.toJSONString(obj, SerializerFeature.WriteMapNullValue,SerializerFeature.WriteNullStringAsEmpty);
        }
    });
    ...
}
</pre></div></div>
                                        <p>上面示例是将null字符串变成""，如果是给安卓app提供接口，这个会很管用。</p>
                                        <h2><a class="anchor" id="改变json格式_1" href="#改变json格式_1"></a>改变json格式</h2>
                                        <p>easyopen默认的返回类是ApiResult，解析成json格式为：</p>
                                        <div class="white"><div class="highlight"><pre>{
    "code": "0",
    "msg": "",
    "data": {...}
}
</pre></div></div>
                                        <p>我们也可以自定义返回结果，比如我们要返回这样的json：</p>
                                        <div class="white"><div class="highlight"><pre>{
    "errCode":"0",
    "errMsg":"",
    "body":{...}
}
</pre></div></div>
                                        <ul class="task-list">
                                            <li>首先，新建结果类，实现com.gitee.easyopen.Result接口：</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>import com.gitee.easyopen.Result;

public class MyResult implements Result {
    private static final long serialVersionUID = -6618981510574135069L;

    private String errCode;
    private String errMsg;
    private String body;

    @Override
    public void setCode(Object code) {
        this.setErrCode(String.valueOf(code));
    }

    @Override
    public void setMsg(String msg) {
        this.setErrMsg(msg);
    }

    @Override
    public void setData(Object data) {
        this.setBody(String.valueOf(data));
    }

    public String getErrCode() {
        return errCode;
    }

    public void setErrCode(String errCode) {
        this.errCode = errCode;
    }

    public String getErrMsg() {
        return errMsg;
    }

    public void setErrMsg(String errMsg) {
        this.errMsg = errMsg;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }
}
</pre></div></div>
                                        <p>MyResult类可定义自己想要的字段名字，实现Result接口对应的方法即可</p>

                                        <ul class="task-list">
                                            <li>然后，新建一个结果生成器，实现ResultCreator接口：</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>import com.gitee.easyopen.Result;
import com.gitee.easyopen.ResultCreator;

public class MyResultCreator implements ResultCreator {

    @Override
    public Result createResult(Object returnObj) {
        MyResult ret = new MyResult();
        ret.setCode(0);
        ret.setData(returnObj);
        return ret;
    }

    @Override
    public Result createErrorResult(Object code, String errorMsg, Object data) {
        MyResult ret = new MyResult();
        ret.setCode(code);
        ret.setMsg(errorMsg);
        ret.setData(data);
        return ret;
    }

}
</pre></div></div>
                                        <p>ResultCreator接口定义两个方法，createResult是返回正确内容的方法，createErrorResult是返回错误时候的方法。
                                            分别实现它们，用我们刚才新建的MyResult类。</p>

                                        <ul class="task-list">
                                            <li>最后，配置结果生成器，在initApiConfig方法中配置：</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>@Controller
@RequestMapping("/project/api")
public class IndexController extends ApiController {

    @Override
    protected void initApiConfig(ApiConfig apiConfig) {

        // 配置结果生成器
        apiConfig.setResultCreator(new MyResultCreator());
        省略其它代码...
    }

}
</pre></div></div>
                                        <p>调用apiConfig.setResultCreator(new MyResultCreator());进行配置</p>
                                        <h1><a class="anchor" id="自定义序列化_1" href="#自定义序列化_1"></a>自定义序列化</h1>
                                        <p>easyopen序列化使用fastjson处理json，xstream处理xml。现在我们来自定义实现一个json处理：</p>

                                        <ul class="task-list">
                                            <li>新建一个类JsonFormatter，实现ResultSerializer接口</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>public class JsonFormatter implements ResultSerializer {
    @Override
    public String serialize(Object obj) {
        Gson gson = new Gson();
        return gson.toJson(obj);
    }
}

</pre></div></div>
                                        <p>这里使用了Gson。</p>

                                        <ul class="task-list">
                                            <li>在apiConfig中配置</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>@Controller
@RequestMapping("/project/api")
public class IndexController extends ApiController {

    @Override
    protected void initApiConfig(ApiConfig apiConfig) {
        // 自定义json序列化
        apiConfig.setJsonResultSerializer(new JsonFormatter());
        省略其它代码...
    }

}
</pre></div></div><h1><a class="anchor" id="定义接口版本号_1" href="#定义接口版本号_1"></a>定义接口版本号</h1>
                                        <p>设置@Api注解的version属性，不指定默认为""。<a href="/api" class="gfm gfm-team_member ">@pytho_njay </a>(name = "goods.get" , version = "2.0")</p>
                                        <h1><a class="anchor" id="在业务类中获取request对象_1" href="#在业务类中获取request对象_1"></a>在业务类中获取Request对象</h1><div class="white"><div class="highlight"><pre>HttpServletRequest request = ApiContext.getRequest();
</pre></div></div><h1><a class="anchor" id="接口交互详解_1" href="#接口交互详解_1"></a>接口交互详解</h1><h2><a class="anchor" id="请求参数_1" href="#请求参数_1"></a>请求参数</h2>
                                        <p>easyopen定义了7个固定的参数，用json接收</p>
                                        <div class="white"><div class="highlight"><pre>{
    "name":"goods.get",
    "version":"2.0",
    "app_key":"test",
    "data":"%7B%22goods_name%22%3A%22iphone6%22%7D",
    "format":"json",
    "timestamp":"2018-01-16 17:02:02",
    "sign":"4CB446DF67DB3637500D4715298CE4A3"
}
</pre></div></div>
                                        <ul class="task-list">
                                            <li>name:接口名称</li>
                                            <li>version：接口版本号</li>
                                            <li>app_key：分配给客户端的app_key</li>
                                            <li>data：业务参数，json格式并且urlencode</li>
                                            <li>format：返回格式，json,xml两种</li>
                                            <li>timestamp：时间戳，yyyy-MM-dd HH:mm:ss</li>
                                            <li>sign：签名串</li>
                                        </ul>

                                        <p>其中sign需要使用双方约定的签名算法来生成。</p>
                                        <h2><a class="anchor" id="请求方式_1" href="#请求方式_1"></a>请求方式</h2>
                                        <p>请求数据放在body体中，采用json格式。这里给出一个POST工具类：</p>
                                        <div class="white"><div class="highlight"><pre>public class PostUtil {

    private static final String UTF8 = "UTF-8";
    private static final String CONTENT_TYPE_JSON = "application/json";
    private static final String ACCEPT_LANGUAGE = "Accept-Language";


    /**
     * POST请求
     * @param url
     * @param params
     * @param lang 语言zh,en
     * @return
     * @throws Exception
     */
    public static String post(String url, JSONObject params, String lang) throws Exception {
        String encode = UTF8;
        // 使用 POST 方式提交数据
        PostMethod method = new PostMethod(url);
        try {
            String requestBody = params.toJSONString();
            // 请求数据放在body体中，采用json格式
            method.setRequestEntity(new StringRequestEntity(requestBody, CONTENT_TYPE_JSON, encode));
            // 设置请求语言
            method.setRequestHeader(ACCEPT_LANGUAGE, lang);

            HttpClient client = new HttpClient();
            int state = client.executeMethod(method); // 返回的状态

            if (state != HttpStatus.SC_OK) {
                throw new Exception("HttpStatus is " + state);
            }

            String response = method.getResponseBodyAsString();

            return response; // response就是最后得到的结果
        } catch (Exception e) {
            throw e;
        } finally {
            method.releaseConnection();
        }
    }

}
</pre></div></div>
                                        <ul class="task-list">
                                            <li>请求操作代码：</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>@Test
public void testGet() throws Exception {

    Map&lt;String, String&gt; param = new HashMap&lt;String, String&gt;();

    Goods goods = new Goods();
    String data = JSON.toJSONString(goods);
    data = URLEncoder.encode(data, "UTF-8");

    param.put("name", "hello");
    param.put("app_key", appId);
    param.put("data", data);
    param.put("timestamp", getTime());
    param.put("version", "");
    param.put("format", "json");

    String sign = ApiUtil.buildSign(param, secret);

    param.put("sign", sign);

    System.out.println("请求内容:" + JSON.toJSONString(param));

    String resp = PostUtil.post(url, param,"zh");

    System.out.println(resp);
}

public String getTime() {
    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
</pre></div></div><h2><a class="anchor" id="签名算法_1" href="#签名算法_1"></a>签名算法</h2>
                                        <p>签名算法描述如下：</p>

                                        <ol class="task-list">
                                            <li>将请求参数按参数名升序排序；</li>
                                            <li>按请求参数名及参数值相互连接组成一个字符串：<code>&lt;paramName1&gt;&lt;paramValue1&gt;&lt;paramName2&gt;&lt;paramValue2&gt;...</code>；</li>
                                            <li>将应用密钥分别添加到以上请求参数串的头部和尾部：<code>&lt;secret&gt;&lt;请求参数字符串&gt;&lt;secret&gt;</code>；</li>
                                            <li>对该字符串进行MD5运算，得到一个二进制数组；</li>
                                            <li>将该二进制数组转换为十六进制的字符串(全部大写)，该字符串即是这些请求参数对应的签名；</li>
                                            <li>该签名值使用<code>sign</code>参数一起和其它请求参数一起发送给服务开放平台。</li>
                                        </ol>

                                        <p>伪代码:</p>
                                        <div class="white"><div class="highlight"><pre>Map&lt;String,Object&gt; paramsMap = new ...; // 参数

Set&lt;String&gt; keySet = paramsMap.keySet();
List&lt;String&gt; paramNames = new ArrayList&lt;String&gt;(keySet);
// 1.
Collections.sort(paramNames);

StringBuilder paramNameValue = new StringBuilder();
// 2.
for (String paramName : paramNames) {
    paramNameValue.append(paramName).append(paramsMap.get(paramName));
}
// 3.
String source = secret + paramNameValue.toString() + secret;
// 4.&amp; 5.
String sign = md5(source);
// 6.
paramsMap.put("sign",sign);

</pre></div></div><h2><a class="anchor" id="服务端验证_1" href="#服务端验证_1"></a>服务端验证</h2>
                                        <p>服务端拿到请求数据后会sign字段进行验证，验证步骤如下：</p>

                                        <ol class="task-list">
                                            <li>根据客户端传过来的app_key拿到服务端保存的secret</li>
                                            <li>拿到secret后通过签名算法生成服务端的serverSign</li>
                                            <li>比较客户端sign和serverSign是否相等，如果相等则证明客户端传来的数据是合法数据，否则不通过返回错误信息。</li>
                                            <li>处理业务，返回结果</li>
                                        </ol>
                                        <h1><a class="anchor" id="忽略验证_1" href="#忽略验证_1"></a>忽略验证</h1>
                                        <p>如果某个接口不需要进行验证工作，可以在@Api注解上设置属性ignoreValidate=true（默认false）。这样调用接口时，不会进行验证操作。</p>

                                        <p>同样的，在@ApiService注解里也有一个对应的ignoreValidate属性，设置为true的话，Service类下面所有的接口都忽略验证。</p>
                                        <h2><a class="anchor" id="忽略所有接口验证_1" href="#忽略所有接口验证_1"></a>忽略所有接口验证</h2>
                                        <p>设置ApiConfig.setIgnoreValidate(true)，所有接口的签名认证操作都将忽略（业务参数校验除外）。</p>
                                        <h1><a class="anchor" id="生成文档页面_1" href="#生成文档页面_1"></a>生成文档页面</h1>
                                        <p>easyopen提供一个简单的api文档查看页面，类似于swagger，基于注解功能来生成文档页面。生成的文档页面可以查看参数、结果说明，也可以进行模拟请求。对于前后端分离的项目来说会很有帮助。文档界面如下图所示：</p>

                                        <p><img alt="输入图片说明" src="https://gitee.com/uploads/images/2018/0203/145842_55f2794e_332975.png" title="3.png" /></p>

                                        <p>左边的树形菜单对应文档名称，点击树可前往查看对应的接口说明。点击请求按钮可以发起请求进行测试。可修改业务参数中的值进行测试。</p>

                                        <p>下面来讲解文档注解的使用方法。</p>

                                        <p>文档页面默认是关闭的，需要在ApiConfig中设置</p>
                                        <div class="white"><div class="highlight"><pre>apiConfig.setShowDoc(true);// 为true开启文档页面。
</pre></div></div>
                                        <p>接下来对获取商品接口设置文档信息：</p>
                                        <div class="white"><div class="highlight"><pre>@Api(name = "goods.get")
public Goods getGoods(GoodsParam param) {
    ...
    return goods;
}
</pre></div></div><h2><a class="anchor" id="设置文档注解_1" href="#设置文档注解_1"></a>设置文档注解</h2>
                                        <p>在接口方法上添加一个@ApiDocMethod注解。</p>
                                        <div class="white"><div class="highlight"><pre>@Api(name = "goods.get")
@ApiDocMethod(description="获取商品") // 文档注解，description为接口描述
public Goods getGoods(GoodsParam param) {
    ...
    return goods;
}
</pre></div></div><h2><a class="anchor" id="设置参数注解_1" href="#设置参数注解_1"></a>设置参数注解</h2>
                                        <p>进入GoodsParam类，使用@ApiDocField注解</p>
                                        <div class="white"><div class="highlight"><pre>public class GoodsParam {

    @ApiDocField(description = "商品名称", required = true, example = "iphoneX")
    private String goods_name;
    // 省略 get set
}
</pre></div></div>
                                        <p>@ApiDocField注解用来定义字段信息，@ApiDocField注解定义如下</p>
                                        <div class="white"><div class="highlight"><pre>/**
     * 字段描述
     */
    String description() default "";

    /**
     * 字段名
     */
    String name() default "";

    /**
     * 数据类型，int string float
     * @return
     */
    DataType dataType() default DataType.STRING;

    /**
     * 是否必填
     */
    boolean required() default false;
</pre></div></div><h2><a class="anchor" id="设置返回结果注解_1" href="#设置返回结果注解_1"></a>设置返回结果注解</h2>
                                        <p>同样，在Goods类中设置@ApiDocField</p>
                                        <div class="white"><div class="highlight"><pre>public class Goods {
    @ApiDocField(description = "id")
    private Long id;
    @ApiDocField(description = "商品名称")
    private String goods_name;
    @ApiDocField(description = "价格", dataType = DataType.FLOAT)
    private BigDecimal price;
    // 省略 get set
}
</pre></div></div>
                                        <p>到此已经设置完毕了，可以访问url进行预览</p>
                                        <h2><a class="anchor" id="文档页面url_1" href="#文档页面url_1"></a>文档页面URL</h2>
                                        <p>文档页面的url格式为：apiUrl + "/doc"。如：apiUrl为<a target="_blank" href="http://localhost:8080/api/v1，那么文档页面就是：http://localhost:8080/api/v1/doc">http://localhost:8080/api/v1，那么文档页面就是：http://localhost:8080/api/v1/doc</a></p>
                                        <h2><a class="anchor" id="list返回_1" href="#list返回_1"></a>List返回</h2>
                                        <p>如果接口方法返回一个List，设置方式如下</p>
                                        <div class="white"><div class="highlight"><pre>    @Api(name = "goods.list", version = "2.0")
    @ApiDocMethod(description="获取商品列表"
        ,results={@ApiDocField(description="商品列表",name="list", elementClass=Goods.class)}
    )
    public List&lt;Goods&gt; listGoods(GoodsParam param) {

    }
</pre></div></div>
                                        <p>elementClass对应List中的元素类型</p>
                                        <h2><a class="anchor" id="设置文档密码-v1-8-5-_1" href="#设置文档密码-v1-8-5-_1"></a>设置文档密码（v1.8.5）</h2><div class="white"><div class="highlight"><pre>apiConfig.setDocPassword("doc123"); // 设置文档页面密码
</pre></div></div>
                                        <p>设置完成后，需要输入密码才能访问文档页面，可配合线上联调，排错。</p>
                                        <h2><a class="anchor" id="修改文档页返回字段名-v1-8-5-_1" href="#修改文档页返回字段名-v1-8-5-_1"></a>修改文档页返回字段名（v1.8.5）</h2>
                                        <p>文档页面默认返回字段名为code,msg,data。可在IndexController中重写<code>processDocVelocityContext()</code>定义</p>
                                        <div class="white"><div class="highlight"><pre>@Override
    protected void processDocVelocityContext(VelocityContext context) {
        context.put("code_name", "errCode");
        context.put("code_description", "状态值，\"0\"表示成功，其它都是失败");

        context.put("msg_name", "errMsg");
        context.put("msg_description", "错误信息，出错时显示");

        context.put("data_name", "respData");
        context.put("data_description", "返回的数据，没有则返回{}");
    }
</pre></div></div><h2><a class="anchor" id="第三方类返回_1" href="#第三方类返回_1"></a>第三方类返回</h2>
                                        <p>如果有个一个PageInfo类，是第三方jar中的，没办法对其修改，那要如何对它里面的属性编写对应文档呢。</p>

                                        <p>PageInfo类内容如下：</p>
                                        <div class="white"><div class="highlight"><pre>// 假设这是jar中的类，没法修改。但是要对其进行文档生成
public class PageInfo&lt;T&gt; {
    private int pageIndex;
    private int pageSize;
    private long total;
    // 省略 get set
}
</pre></div></div>
                                        <p>我们可以显式的声明字段信息：</p>
                                        <div class="white"><div class="highlight"><pre>    @Api(name = "goods.pageinfo", version = "1.0")
    @ApiDocMethod(description="获取商品列表"
            ,results={@ApiDocField(name="pageIndex",description="第几页",dataType=DataType.INT,example="1"),
                    @ApiDocField(name="pageSize",description="每页几条数据",dataType=DataType.INT,example="10"),
                    @ApiDocField(name="total",description="每页几条数据",dataType=DataType.LONG,example="100"),
                    @ApiDocField(name="rows",description="数据",dataType=DataType.ARRAY,elementClass=Goods.class),}
            )
    public PageInfo&lt;Goods&gt; pageinfo(GoodsParam param) {

    }
</pre></div></div><h2><a class="anchor" id="文档模型复用_1" href="#文档模型复用_1"></a>文档模型复用</h2>
                                        <p>如果多个接口都返回PageInfo，需要复制黏贴大量的注解，改一个地方需要改多个接口，无法达到复用效果。我们可以新建一个GoodsPageVo继承PageInfo，然后把文档注解写在类的头部，这样可以达到复用效果。</p>
                                        <div class="white"><div class="highlight"><pre>@ApiDocBean(fields = {
    @ApiDocField(name="pageIndex",description="第几页",dataType=DataType.INT,example="1"),
    @ApiDocField(name="pageSize",description="每页几条数据",dataType=DataType.INT,example="10"),
    @ApiDocField(name="total",description="每页几条数据",dataType=DataType.LONG,example="100"),
    @ApiDocField(name="rows",description="商品列表",dataType=DataType.ARRAY,elementClass=Goods.class),
})
public class GoodsPageVo extends PageInfo&lt;Goods&gt; {

}
</pre></div></div><div class="white"><div class="highlight"><pre>    @Api(name = "goods.pageinfo", version = "2.0")
    @ApiDocMethod(description="获取商品列表",resultClass=GoodsPageVo.class)
    public PageInfo&lt;Goods&gt; pageinfo2(GoodsParam param) {

    }
</pre></div></div>
                                        <p>使用resultClass=GoodsPageVo.class指定返回结果类型即可。</p>
                                        <h2><a class="anchor" id="更改文档显示位置-v1-9-1-_1" href="#更改文档显示位置-v1-9-1-_1"></a>更改文档显示位置（v1.9.1）</h2>
                                        <p>可以使用<code>order</code>属性来指定文档显示位置，值越小越靠前</p>
                                        <div class="white"><div class="highlight"><pre>@ApiDoc(value = "文档demo，参考DocDemoApi.java", order = 1)

@ApiDocMethod(description = "第一个",  order = 1 // 指定了order，优先按这个值排序
    )

若不指定order，则按接口名排序
</pre></div></div><h1><a class="anchor" id="使用oauth2_1" href="#使用oauth2_1"></a>使用oauth2</h1>
                                        <p>如果第三方应用和本开放平台对接时需要获取用户隐私数据（如商品、订单），为为了安全与隐私，第三方应用需要取得用户的授权，即获取访问用户数据的授权令牌 AccessToken 。这种情况下，第三方应用需要引导用户完成帐号“登录授权”的流程。</p>

                                        <p>easyopen从1.2.0版本开始支持oauth2认证。接入方式很简单：</p>

                                        <ol class="task-list">
                                            <li>新建一个Oauth2ManagerImpl类，实现Oauth2Manager接口</li>
                                            <li>用户类实现OpenUser接口。</li>
                                        </ol>
                                        <div class="white"><div class="highlight"><pre>@Service
public class Oauth2ManagerImpl implements Oauth2Manager {
...
}

public class User implements OpenUser {
...
}
</pre></div></div>
                                        <p>因为对于accessToken的管理每个开发人员所用的方式都不一样，所以需要自己来实现。</p>

                                        <ul class="task-list">
                                            <li>Oauth2Manager接口定义如下：</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>public interface Oauth2Manager {

    /**
     * 添加 auth code
     *
     * @param authCode
     *            code值
     * @param authUser
     *            用户
     */
    void addAuthCode(String authCode, OpenUser authUser);

    /**
     * 添加 access token
     *
     * @param accessToken
     *            token值
     * @param authUser
     *            用户
     * @param expiresIn 时长,秒
     */
    void addAccessToken(String accessToken, OpenUser authUser, long expiresIn);

    /**
     * 验证auth code是否有效
     *
     * @param authCode
     * @return 无效返回false
     */
    boolean checkAuthCode(String authCode);

    /**
     * 根据auth code获取用户
     *
     * @param authCode
     * @return 返回用户
     */
    OpenUser getUserByAuthCode(String authCode);

    /**
     * 根据access token获取用户名
     *
     * @param accessToken
     *            token值
     * @return 返回用户
     */
    OpenUser getUserByAccessToken(String accessToken);

    /**
     * 获取auth code / access token 过期时间
     *
     * @return
     */
    long getExpireIn(ApiConfig apiConfig);

    /**
     * 用户登录，需判断是否已经登录
     * @param request
     * @return 返回用户对象
     */
    OpenUser login(HttpServletRequest request) throws LoginErrorException;
}
</pre></div></div><h2><a class="anchor" id="accesstoken获取流程_1" href="#accesstoken获取流程_1"></a>accessToken获取流程</h2>
                                        <ul class="task-list">
                                            <li>第一步获取code</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>1、首先通过如http://localhost:8080/api/authorize?client_id=test&amp;response_type=code&amp;redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Foauth2callback访问授权页面；
2、该控制器首先检查clientId是否正确；如果错误将返回相应的错误信息；
3、然后判断用户是否登录了，如果没有登录首先到登录页面登录；
4、登录成功后生成相应的code即授权码，然后重定向到客户端地址，如http://localhost:8080/oauth2callback?code=6d250650831fea227749f49a5b49ccad；在重定向到的地址中会带上code参数（授权码），接着客户端可以根据授权码去换取accessToken。
</pre></div></div>
                                        <ul class="task-list">
                                            <li>第二步通过code换取accessToken</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>1、首先通过如http://localhost:8080/api/accessToken，POST提交如下数据访问:

        code:6d250650831fea227749f49a5b49ccad
        client_id:test
        client_secret:123456
        grant_type:authorization_code
        redirect_uri:http://localhost:8080/api/authorize

2、服务器会验证client_id、client_secret、code的正确性，如果错误会返回相应的错误；
3、如果验证通过会生成并返回相应的访问令牌accessToken。

{
  "access_token": "01e111c0d3c8e415fea038d5c64432ef",
  "refresh_token": "d1165b75d386b3ef1bd0423b4e3bfef9",
  "token_type": "Bearer",
  "expires_in": 7200,
  "username": "admin"
}
</pre></div></div>
                                        <p>以上两个步骤需要在客户端上实现。示例项目easyopen-server上有个例子可以参考，启动服务，然后访问<a target="_blank" href="http://localhost:8080/go_oauth2">http://localhost:8080/go_oauth2</a></p>

                                        <p>获取accessToken用户：</p>
                                        <div class="white"><div class="highlight"><pre>// 拿到accessToken用户
OpenUser user = ApiContext.getAccessTokenUser();
</pre></div></div><h2><a class="anchor" id="使用refreshtoken刷新accesstoken_1" href="#使用refreshtoken刷新accesstoken_1"></a>使用refreshToken刷新accessToken</h2>
                                        <p>accessToken有过期时间，为了防止过期可以通过refreshToken来换取新的accessToken，方便后续接口调用。</p>
                                        <div class="white"><div class="highlight"><pre>1. 首先通过如http://localhost:8080/api/accessToken，POST提交如下数据访问:
        refresh_token:你的refreshToken
        client_id:test
        client_secret:123456
        grant_type:refresh_token
2. 服务器会验证client_id、client_secret、refresh_token的正确性，如果错误会返回相应的错误；
3. 如果验证通过会生成并返回新的访问令牌accessToken和新的refreshToken

返回结果：
{
  "access_token": "01e111c0d3c8e415fea038d5c64432ef",
  "refresh_token": "d1165b75d386b3ef1bd0423b4e3bfef9",
  "token_type": "Bearer",
  "expires_in": 7200,
  "username": "admin"
}

</pre></div></div>
                                        <p>成功换取新的accessToken和refreshToken后，老的accessToken和refreshToken不能使用。</p>
                                        <h1><a class="anchor" id="使用jwt_1" href="#使用jwt_1"></a>使用JWT</h1>
                                        <p>JWT的介绍参考这里：<a href="https://www.jianshu.com/p/576dbf44b2ae">什么是 JWT -- JSON WEB TOKEN</a>。</p>

                                        <p>之前我们的web应用使用session来维持用户与服务器之间的关系，其原理是使用一段cookie字符与服务器中的一个Map来对应，Map，用户每次交互需要带一个sessionid过来。如果不使用分布式session，一旦服务器重启session会丢失，这样会影响用户体验，甚至影响业务逻辑。如果把用户信息存在客户端就没这个问题了。</p>

                                        <p>easyopen创建JWT方式如下：</p>
                                        <div class="white"><div class="highlight"><pre>Map&lt;String, String&gt; data = new HashMap&lt;&gt;();
data.put("id", user.getId().toString());
data.put("username", user.getUsername());

String jwt = ApiContext.createJwt(data);
</pre></div></div>
                                        <p>这段代码用在用户登录的时候，然后把jwt返回给客户端，让客户端保存，如H5可以存在localStorage中。</p>

                                        <p>客户端传递jwt方式：</p>
                                        <div class="white"><div class="highlight"><pre>method.setRequestHeader("Authorization", "Bearer " + jwt);
</pre></div></div>
                                        <p>即在header头部添加一个Authorization，内容是"Bearer " + jwt</p>

                                        <p>客户端请求过来后，服务端获取jwt中的数据：</p>
                                        <div class="white"><div class="highlight"><pre>// 获取jwt数据
Map&lt;String, Claim&gt; jwtData = ApiContext.getJwtData();
</pre></div></div><h1><a class="anchor" id="拦截器_1" href="#拦截器_1"></a>拦截器</h1>
                                        <p>easyopen在1.3.1版本开始支持拦截器。</p>

                                        <p>easyopen拦截器实现原理跟springmvc拦截器类似，拦截器作用在api方法上，即有@Api注解的方法。</p>

                                        <p>拦截器定义如下：</p>
                                        <div class="white"><div class="highlight"><pre>/**
 * 拦截器，原理同springmvc拦截器
 * @author tanghc
 *
 */
public interface ApiInterceptor {
    /**
     * 预处理回调方法，在方法调用前执行
     * @param request
     * @param response
     * @param serviceObj service类
     * @param argu 方法参数
     * @return
     * @throws Exception
     */
    boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object serviceObj, Object argu)
            throws Exception;

    /**
     * 接口方法执行完后调用此方法。
     * @param request
     * @param response
     * @param serviceObj service类
     * @param argu 参数
     * @param result 方法返回结果
     * @throws Exception
     */
    void postHandle(HttpServletRequest request, HttpServletResponse response, Object serviceObj, Object argu,
            Object result) throws Exception;

    /**
     * 结果包装完成后执行
     * @param request
     * @param response
     * @param serviceObj service类
     * @param argu 参数
     * @param result 最终结果，被包装过
     * @param e
     * @throws Exception
     */
    void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object serviceObj, Object argu,
            Object result, Exception e) throws Exception;

    /**
     * 匹配拦截器
     * @param apiMeta 接口信息
     * @return
     */
    boolean match(ApiMeta apiMeta);
}
</pre></div></div>
                                        <p>同样也提供一个适配器ApiInterceptorAdapter</p>

                                        <ul class="task-list">
                                            <li>拦截器执行流程：</li>
                                        </ul>

                                        <p>跟springmvc拦截器执行流程一样</p>

                                        <ol class="task-list">
                                            <li>preHandle 如果返回false，则不调用api方法，接着逆序调用afterCompletion，需要通过response自定义返回</li>
                                            <li>如果preHandle 返回true，继续进行下一个preHandle</li>
                                            <li>preHandle执行完毕后，逆序执行postHandle</li>
                                            <li>最后逆序调用afterCompletion</li>
                                        </ol>

                                        <ul class="task-list">
                                            <li>正常流程：</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>ApiInterceptor1.preHandle
ApiInterceptor2.preHandle

apiMethod.invoke() // api方法调用

ApiInterceptor2.postHandle
ApiInterceptor1.postHandle

ApiInterceptor2.afterCompletion
ApiInterceptor1.afterCompletion
</pre></div></div><h2><a class="anchor" id="配置拦截器_1" href="#配置拦截器_1"></a>配置拦截器</h2>
                                        <p>新建一个日志处理拦截器，继承ApiInterceptorAdapter，重写父类中的方法</p>
                                        <div class="white"><div class="highlight"><pre>public class LogInterceptor extends ApiInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object serviceObj, Object argu)
            throws Exception {

        System.out.println("======preHandle======");
        System.out.println("IP:" + RequestUtil.getClientIP(request));
        System.out.println("接口类：" + serviceObj.getClass().getName());
        if(argu != null) {
            System.out.println("参数类：" + argu.getClass().getName());
        }


        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object serviceObj, Object argu,
            Object result) throws Exception {
        System.out.println("======postHandle======");
        System.out.println("接口类：" + serviceObj.getClass().getName());
        if(argu != null) {
            System.out.println("参数类：" + argu.getClass().getName());
        }
        System.out.println("结果：" + JSON.toJSONString(result));
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object serviceObj,
            Object argu, Object result, Exception e) throws Exception {
        System.out.println("======afterCompletion======");
        System.out.println("接口类：" + serviceObj.getClass().getName());
        System.out.println("参数类：" + argu.getClass().getName());
        System.out.println("最终结果：" + JSON.toJSONString(result));
        System.out.println("e:" + e);
    }

}
</pre></div></div>
                                        <p>在apiConfgi中添加拦截器:</p>
                                        <div class="white"><div class="highlight"><pre>@Override
    protected void initApiConfig(ApiConfig apiConfig) {
        ...

        // 配置拦截器
        apiConfig.setInterceptors(
                new ApiInterceptor[] { new LogInterceptor()});

       ...
    }
</pre></div></div><h2><a class="anchor" id="拦截范围_1" href="#拦截范围_1"></a>拦截范围</h2>
                                        <p>默认拦截所有接口，如果要拦截指定接口，可重写boolean match()方法：</p>
                                        <div class="white"><div class="highlight"><pre>// 只拦截goods.get接口
@Override
    public boolean match(ApiMeta apiMeta) {
        return apiMeta.getName().equals("goods.get");
    }
</pre></div></div><h1><a class="anchor" id="数据加密传输_1" href="#数据加密传输_1"></a>数据加密传输</h1>
                                        <p>默认我们的数据传输都是不经过加密的，要加密传输的话得用上HTTPS协议。easyopen在1.4.0版本开始提供了数据加密传输，不需要HTTPS协议。</p>

                                        <p>easyopen基于公私钥+AES加密传输，交互流程如下：</p>
                                        <div class="white"><div class="highlight"><pre>0. 事先把公钥放在客户端,私钥放在服务端
1. 客户端生成一个随机码randomKey
2. 将randomKey通过公钥RSA加密str = rsa_encode(randomKey,publicKey)
3. 将str发送到服务端
4. 服务端通过私钥解开str,得到randomKey:randomKey = rsa_decode(str, privateKey)
5. 服务端使用randomKey通过AES对称加密一个值(假设值为"0")返回给客户端,resp = aes_encode("0", randomKey)
6. 客户端用自己的randomKey去aes解密resp,如果得到的是"0",则整个握手交互完成,后续都用这个randomKey进行aes加解密传输

注:
黑客是可以拿到公钥的,但是黑客无法知道客户端生成的随机码randomKey,一旦str发生改变则握手失败.
整个过程的重点就是将randomKey安全的送到服务器,后期都用randomKey进行对称加密传输,对称加密黑客无法破解.
此流程参照HTTPS,只不过服务器无法将公钥正确的送到客户端(浏览器),因此浏览器的HTTPS需要CA机构介入.
</pre></div></div><h2><a class="anchor" id="公私钥配置_1" href="#公私钥配置_1"></a>公私钥配置</h2>
                                        <ul class="task-list">
                                            <li>生成公私钥</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>public class PubPriKeyTest extends TestCase {
    /**
     * 生成公私钥
     * @throws Exception
     */
    @Test
    public void testCreate() throws Exception {
        KeyStore store = RSAUtil.createKeys();
        System.out.println("公钥:");
        System.out.println(store.getPublicKey());
        System.out.println("私钥:");
        System.out.println(store.getPrivateKey());
    }
}
</pre></div></div>
                                        <ul class="task-list">
                                            <li>在客户端新建一个pub.key文件，放入公钥字符串，pub.key放在客户端，启动的时候加载。</li>
                                            <li>在服务端新建一个pri.key文件，放入私钥字符串，pri.key放在resources目录下，服务启动会自动读取。</li>
                                        </ul>

                                        <p>easyopen-sdk的resources下已经存放了一个pub.key，实例化一个EncryptClient对象就能使用数据加密传输。感兴趣的同学可以查看源码，了解整个交互流程。</p>
                                        <div class="white"><div class="highlight"><pre>// 数据加密传输不需要secret
OpenClient client = new EncryptClient(url, app_key);
</pre></div></div><h1><a class="anchor" id="可自定义默认版本号_1" href="#可自定义默认版本号_1"></a>可自定义默认版本号</h1>
                                        <p>接口默认的版本号是空字符串""，如果要修改，可进行如下配置：</p>
                                        <div class="white"><div class="highlight"><pre>// 修改默认版本号，所有的接口的默认版本变为1.0
apiConifg.setDefaultVersion("1.0");
</pre></div></div><h1><a class="anchor" id="自定义session管理_1" href="#自定义session管理_1"></a>自定义session管理</h1>
                                        <p>easyopen1.4.0开始支持。</p>

                                        <ul class="task-list">
                                            <li>创建session</li>
                                        </ul>

                                        <p>登陆成功后创建session，并返回sessionId</p>
                                        <div class="white"><div class="highlight"><pre>// 自定义session
    @PostMapping("managedSessionLogin")
    public String managedSessionLogin(HttpServletRequest request) {
        // 假设登陆成功,创建一个sessionId返回给客户端
        SessionManager sessionManager = ApiContext.getApiConfig().getSessionManager();
        HttpSession session = sessionManager.getSession(null);
        session.setAttribute("username", "tom");

        return session.getId();
    }
</pre></div></div>
                                        <ul class="task-list">
                                            <li>使用session</li>
                                        </ul>

                                        <p>客户端需要传递access_token参数，值为sessionId</p>

                                        <p>请求参数：</p>
                                        <div class="white"><div class="highlight"><pre>{
    "access_token": "4191FB1D8356495D98ECCF91C615A530",
    "app_key": "test",
    "data": "{}",
    "sign": "F7AB6BC059DFCA93CA2328C9BAF236BA",
    "sign_method": "md5",
    "name": "manager.session.get",
    "format": "json",
    "version": "",
    "timestamp": "2018-03-13 13:48:45"
}
</pre></div></div>
                                        <p>服务端通过HttpSession session = ApiContext.getManagedSession();获取session</p>
                                        <div class="white"><div class="highlight"><pre>@Api(name = "manager.session.get")
    public Object managersetsession() {
        HttpSession session = ApiContext.getManagedSession();

        System.out.println(session.getId());

        Object user = session.getAttribute("username");

        return user;
    }
</pre></div></div><h2><a class="anchor" id="使用redis管理session_1" href="#使用redis管理session_1"></a>使用redis管理session</h2>
                                        <p>easyopen默认使用谷歌的guava缓存进行session管理，但是在集群的情况下会有问题，因此easyopen还提供了一个Redis版本。配置如下：</p>

                                        <ul class="task-list">
                                            <li>pom添加redis依赖</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>&lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt;
        &lt;/dependency&gt;
</pre></div></div>
                                        <ul class="task-list">
                                            <li>添加redis参数</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>
#################redis基础配置#################
spring.redis.database=0
spring.redis.host=10.1.11.48
spring.redis.password=0987654321rfvujmtgbyhn
spring.redis.port=6379
# 连接超时时间 单位 ms（毫秒）
spring.redis.timeout=3000

#################redis线程池设置#################
# 连接池中的最大空闲连接，默认值也是8。
spring.redis.pool.max-idle=500
#连接池中的最小空闲连接，默认值也是0。
spring.redis.pool.min-idle=50
# 如果赋值为-1，则表示不限制；如果pool已经分配了maxActive个jedis实例，则此时pool的状态为exhausted(耗尽)。
spring.redis.pool.max-active=2000
# 等待可用连接的最大时间，单位毫秒，默认值为-1，表示永不超时。如果超过等待时间，则直接抛出JedisConnectionException
spring.redis.pool.max-wait=1000
</pre></div></div>
                                        <ul class="task-list">
                                            <li>设置apiConfig</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>@Controller
@RequestMapping(value = "/api")
public class IndexController extends ApiController {

    @Autowired
    private RedisTemplate redisTemplate; // 1 声明redis模板

    @Override
    protected void initApiConfig(ApiConfig apiConfig) {
        // 配置秘钥键值对
        Map&lt;String, String&gt; appSecretStore = new HashMap&lt;String, String&gt;();
        appSecretStore.put("test", "123456");

        // 2 配置sessionManager
        RedisSessionManager sessionManager = new RedisSessionManager(redisTemplate);
        apiConfig.setSessionManager(sessionManager);

        apiConfig.addAppSecret(appSecretStore);
    }

}
</pre></div></div><h3><a class="anchor" id="修改redis的key前缀_1" href="#修改redis的key前缀_1"></a>修改redis的key前缀</h3>
                                        <p>默认存入redis的key前缀为<code>session:</code>，如果要自定义前缀可调用：</p>
                                        <div class="white"><div class="highlight"><pre>sessionManager.setKeyPrefix("session-key:");
</pre></div></div><h1><a class="anchor" id="app-key和secret存放在数据库或redis中_1" href="#app-key和secret存放在数据库或redis中_1"></a>app_key和secret存放在数据库或redis中</h1>
                                        <p>这里以redis为例</p>

                                        <p>新建一个RedisAppSecretManager类实现AppSecretManager接口</p>
                                        <div class="white"><div class="highlight"><pre>/**
 * 使用方式:
 *
 * &lt;pre&gt;
@Autowired
private AppSecretManager appSecretManager;

@Override
protected void initApiConfig(ApiConfig apiConfig) {
    ...
    apiConfig.setAppSecretManager(appSecretManager);
    ...
}

 * &lt;/pre&gt;
 *
 * @author tanghc
 *
 */
@Component
public class RedisAppSecretManager implements AppSecretManager {

    public static String APP_KEY_PREFIX = "easyopen_app_key:";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    @Override
    public void addAppSecret(Map&lt;String, String&gt; appSecretStore) {
        stringRedisTemplate.opsForHash().putAll(APP_KEY_PREFIX, appSecretStore);
    }

    @Override
    public String getSecret(String appKey) {
        return (String)stringRedisTemplate.opsForHash().get(APP_KEY_PREFIX, appKey);
    }

    @Override
    public boolean isValidAppKey(String appKey) {
        if (appKey == null) {
            return false;
        }
        return getSecret(appKey) != null;
    }

}
</pre></div></div>
                                        <p>存放app_key和secret采用hash set的方式,这样在redis中查看会比较方便,一目了然.</p>

                                        <p>然后在IndexController中:</p>
                                        <div class="white"><div class="highlight"><pre>@Autowired
private AppSecretManager appSecretManager;

@Override
protected void initApiConfig(ApiConfig apiConfig) {
    ...
    apiConfig.setAppSecretManager(appSecretManager);
    ...
}
</pre></div></div><h1><a class="anchor" id="使用webflux_1" href="#使用webflux_1"></a>使用WebFlux</h1>
                                        <p>这里基于springboot2 + WebFlux,相关教程见:<a href="https://www.ibm.com/developerworks/cn/java/spring5-webflux-reactive/index.html">springboot-webflux</a></p>

                                        <p>需要easyopen1.7.0及以上版本</p>

                                        <ul class="task-list">
                                            <li>在pom.xml中添加WebFlux依赖</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-webflux&lt;/artifactId&gt;
&lt;/dependency&gt;
</pre></div></div>
                                        <ul class="task-list">
                                            <li>在IndexController中添加:</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>
@Controller
@RequestMapping("/api")
public class IndexController extends ApiController {
    ...

    // http://localhost:8080/api/mono
    @RequestMapping("mono")
    @ResponseBody
    public Mono&lt;Object&gt; mono(HttpServletRequest request, HttpServletResponse response) {
        return Mono.justOrEmpty(this.invoke(request, response));
    }

    ...

}
</pre></div></div>
                                        <p>api的url由之前的<a target="_blank" href="http://localhost:8080/api变为http://localhost:8080/api/mono">http://localhost:8080/api变为http://localhost:8080/api/mono</a></p>

                                        <p>其它地方不变</p>
                                        <h1><a class="anchor" id="开启app对接模式-v1-7-5-_1" href="#开启app对接模式-v1-7-5-_1"></a>开启app对接模式（v1.7.5）</h1>
                                        <p>如果平台直接跟Android或IOS对接，可开启这个功能。因为手机上的时间有可能跟服务端的时间不一致（用户的手机情况不可控）。</p>

                                        <p>失去了时间校验，一个请求有可能被反复调用，服务端需要防止重复提交，有必要的话上HTTPS。</p>

                                        <p>开启方式：</p>
                                        <div class="white"><div class="highlight"><pre>apiConfig.openAppMode();
</pre></div></div>
                                        <p>开启app对接模式，开启后不进行timeout校验。</p>
                                        <h1><a class="anchor" id="防止表单重复提交-v1-7-7-_1" href="#防止表单重复提交-v1-7-7-_1"></a>防止表单重复提交（v1.7.7）</h1>
                                        <p>使用redis分布式锁解决表单重复提交问题。</p>
                                        <div class="white"><div class="highlight"><pre>核心思想：

try {
    锁（用户id + 接口名） {
        执行业务代码
    }
} finally {
    释放锁
}

在锁的内部执行业务代码时，其它线程进来都将拒之门外。

</pre></div></div>
                                        <p>新增拦截器继承BaseLockInterceptor </p>
                                        <div class="white"><div class="highlight"><pre>/**
 * 使用分布式锁防止表单重复提交
 *
 * @author tanghc
 */
public class LockInterceptor extends BaseLockInterceptor {

    private StringRedisTemplate redisTemplate;

    public LockInterceptor() {
        redisTemplate = ApiContext.getApplicationContext().getBean(StringRedisTemplate.class);
    }

    @SuppressWarnings("rawtypes")
    @Override
    protected RedisTemplate getRedisTemplate() {
        return redisTemplate;
    }

    @Override
    protected String getUserId() {
        Map&lt;String, Claim&gt; jwtData = ApiContext.getJwtData();
        String id = jwtData.get("id").asString(); // 用户id
        return id;
    }

    @Override
    public boolean match(ApiMeta apiMeta) {
        return "userlock.test".equals(apiMeta.getName()); // 只针对这个接口
    }
}
</pre></div></div>
                                        <p>实现上面三个方法即可,match方法返回true表示执行这个拦截器，可针对特定的接口做操作。</p>

                                        <p>然后配置拦截器：</p>
                                        <div class="white"><div class="highlight"><pre>apiConfig.setInterceptors(new ApiInterceptor[] {new LockInterceptor()});
</pre></div></div><h1><a class="anchor" id="监控页面-v1-8-1-_1" href="#监控页面-v1-8-1-_1"></a>监控页面（v1.8.1）</h1>
                                        <p>启动服务端，浏览器访问:<a target="_blank" href="http://localhost:8080/api/monitor">http://localhost:8080/api/monitor</a></p>

                                        <p>输入密码:monitor123</p>
                                        <h2><a class="anchor" id="修改默认密码_1" href="#修改默认密码_1"></a>修改默认密码</h2><div class="white"><div class="highlight"><pre>apiConfig.setMonitorPassword(newPassword);
</pre></div></div>
                                        <ul class="task-list">
                                            <li>监控内容放在Map对象中，存放接口对应的累积信息，并不会记录每次请求的信息，因此无需担心内存使用量增多。</li>
                                        </ul>
                                        <h1><a class="anchor" id="文件上传-v1-8-7-_1" href="#文件上传-v1-8-7-_1"></a>文件上传（v1.8.7）</h1>
                                        <ul class="task-list">
                                            <li>SDK</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>/**
 * 上传文件，读取本地文件
 *
 * @throws IOException
 */
@Test
public void testUpload() throws IOException {
    GoodsParam param = new GoodsParam();
    param.setGoods_name("iphone6");
    GoodsReq req = new GoodsReq("file.upload", param);

    String path = this.getClass().getResource("").getPath();

    List&lt;UploadFile&gt; files = new ArrayList&lt;&gt;();

    // 这里的headImg，idcardImg要跟服务端参数名对应
    files.add(new UploadFile("headImg", new File(path + "1.txt")));
    files.add(new UploadFile("idcardImg", new File(path + "2.txt")));

    GoodsResp result = client.requestFile(req, files);

    System.out.println("--------------------");
    if (result.isSuccess()) {
        System.out.println(result.getData());
    } else {
        System.out.println("errorMsg:" + result.getMsg());
    }
    System.out.println("--------------------");
}
</pre></div></div>
                                        <ul class="task-list">
                                            <li>服务端处理</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>@Api(name = "file.upload")
    @ApiDocMethod(description = "文件上传")
    Object upload(UploadParam param) throws IllegalStateException, IOException {
        // 获取上传文件
        MultipartFile headImgFile = param.getHeadImg();
        MultipartFile idcardImgFile = param.getIdcardImg();

        StringBuilder sb = new StringBuilder();
        sb.append("表单名：").append(headImgFile.getName()).append(",")
        .append("文件大小：").append(headImgFile.getSize()).append(";");

        sb.append("表单名：").append(idcardImgFile.getName()).append(",")
        .append("文件大小：").append(idcardImgFile.getSize()).append(";");

        // headImgFile.getInputStream(); // 返回文件流
        // headImgFile.getBytes(); // 返回文件数据流

        headImgFile.transferTo(new File("D:/new_" + headImgFile.getOriginalFilename()));
        idcardImgFile.transferTo(new File("D:/new_" + idcardImgFile.getOriginalFilename()));

        return new ApiResult(sb.toString());
    }
</pre></div></div>
                                        <ul class="task-list">
                                            <li>UploadParam.java</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>public class UploadParam {

    @ApiDocField(description = "商品名称", required = true, example = "iphoneX")
    @NotEmpty(message = "商品名称不能为空")
    @Length(min = 3, max = 20, message = "{goods.name.length}=3,20")
    private String goods_name;

    // 这里定义上传的文件，属性名称对应客户端上传的name
    @ApiDocField(description = "头像图片", required = true, dataType = DataType.FILE)
    @NotNull(message = "请上传头像图片")
    private MultipartFile headImg;

    @ApiDocField(description = "身份证图片", required = true, dataType = DataType.FILE)
    @NotNull(message = "请上传身份证图片")
    private MultipartFile idcardImg;

    //getter,setter

}
</pre></div></div>
                                        <p><code>headImg</code>,<code>idcardImg</code>就是上传的表单名，客户端需要于此对应。</p>
                                        <h2><a class="anchor" id="上传内存文件_1" href="#上传内存文件_1"></a>上传内存文件</h2>
                                        <p>有些文件不是从本地读取的，而是从其它地方下载到内存中，比如从阿里云下载文件到内存中，不落地。</p>
                                        <div class="white"><div class="highlight"><pre>List&lt;UploadFile&gt; files = new ArrayList&lt;&gt;();

files.add(new UploadFile("headImg","headImg.txt", this.getClass().getResourceAsStream("1.txt")));
files.add(new UploadFile("idcardImg", "idcardImg.txt", this.getClass().getResourceAsStream("2.txt")));

GoodsResp result = client.requestFile(req, files);
</pre></div></div>
                                        <p>或者</p>
                                        <div class="white"><div class="highlight"><pre>List&lt;UploadFile&gt; files = new ArrayList&lt;&gt;();

files.add(new UploadFile("headImg","headImg.txt", byte[]));
files.add(new UploadFile("idcardImg", "idcardImg.txt", byte[]));

GoodsResp result = client.requestFile(req, files);
</pre></div></div>
                                        <p>主要通过UploadFile类的构造方法来区分</p>
                                        <div class="white"><div class="highlight"><pre>/**
 * @param name 表单名称，不能重复
 * @param file 文件
 * @throws IOException
 */
public UploadFile(String name, File file)

/**
 * @param name 表单名称，不能重复
 * @param fileName 文件名
 * @param input 文件流
 * @throws IOException
 */
public UploadFile(String name, String fileName, InputStream input)

/**
 * @param name 表单名称，不能重复
 * @param fileName 文件名
 * @param fileData 文件数据
 */
public UploadFile(String name, String fileName, byte[] fileData)
</pre></div></div><h1><a class="anchor" id="限流功能-v1-9-1-_1" href="#限流功能-v1-9-1-_1"></a>限流功能（v1.9.1）</h1>
                                        <p>针对每个接口做限流功能，限流方式有两种：</p>

                                        <ul class="task-list">
                                            <li>漏桶策略：每秒处理固定数量的请求，超出请求返回错误信息。可用在秒杀、抢购业务</li>
                                            <li>令牌桶策略：每秒放置固定数量的令牌数，不足的令牌数做等待处理，直到拿到令牌为止。平滑输出，可减轻服务器压力。</li>
                                        </ul>

                                        <p>两种策略可在后台页面切换</p>
                                        <h2><a class="anchor" id="开启限流功能_1" href="#开启限流功能_1"></a>开启限流功能</h2>
                                        <p>以springboot为例</p>

                                        <ul class="task-list">
                                            <li>application.properties配置redis信息</li>
                                            <li>IndexController中配置：</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>@Autowired
private RedisTemplate redisTemplate;

@Override
protected void initApiConfig(ApiConfig apiConfig) {
    ...

    // 配置拦截器
    apiConfig.setInterceptors(
            new ApiInterceptor[] {
                    new LimitInterceptor() // 限流拦截器,放在首位
                    ...
                    });


    /*
     ****************设置限流管理************************* */
    apiConfig.setLimitManager(new ApiLimitManager(redisTemplate));
    // [可选]，设置配置页面访问密码
    //apiConfig.setLimitPassword(limitPassword);
    // [可选]，设置【策略】，默认为漏桶策略
    //apiConfig.setDefaultLimitType(defaultLimitType);
    // [可选]，设置【每秒可处理请求数】，默认50。策略为漏桶策略时，该值生效
    //apiConfig.setDefaultLimitCount(defaultLimitCount);
    // [可选]，设置【令牌桶容量】，默认50。策略为令牌桶策略时，该值生效
    //apiConfig.setDefaultTokenBucketCount(defaultTokenBucketCount);
}
</pre></div></div>
                                        <ul class="task-list">
                                            <li>访问：<a target="_blank" href="http://localhost:8080/api/limit">http://localhost:8080/api/limit</a>  对接口进行限流设置，默认密码limit123</li>
                                        </ul>
                                        <h1><a class="anchor" id="文档页头部显示项目描述-v1-9-1-_1" href="#文档页头部显示项目描述-v1-9-1-_1"></a>文档页头部显示项目描述（v1.9.1）</h1>
                                        <p>IndexController重写getDocRemark()方法</p>
                                        <div class="white"><div class="highlight"><pre>@Override
protected String getDocRemark() {
    return "文档描述，支持html标签&lt;br&gt;";
}

</pre></div></div><h1><a class="anchor" id="使用get方式请求-v1-9-1-_1" href="#使用get方式请求-v1-9-1-_1"></a>使用GET方式请求（v1.9.1）</h1>
                                        <p>直接在url跟参数</p>

                                        <p>如接口URL为：<a target="_blank" href="http://localhost:8080/api">http://localhost:8080/api</a></p>

                                        <p>则完整地址为：</p>
                                        <div class="white"><div class="highlight"><pre>http://localhost:8080/api?name=doc.result.5&amp;version=&amp;app_key=test&amp;data=%257B%2522goods_name%2522%253A%2522iphoneX%2522%257D&amp;timestamp=2018-06-22%2009%3A38%3A32&amp;format=json&amp;sign=552BB4285F59C4CC230164E8E3BF4348
</pre></div></div>
                                        <ul class="task-list">
                                            <li>注：参数值需要urlencode一下，如果用到上传功能，还是需要post方式</li>
                                        </ul>
                                        <h1><a class="anchor" id="使用配置中心-v1-10-0-_1" href="#使用配置中心-v1-10-0-_1"></a>使用配置中心（v1.10.0）</h1>
                                        <p>配置中心提供的功能</p>

                                        <ul class="task-list">
                                            <li>接入方管理：统一管理appKey、secret、公私钥</li>
                                            <li>权限管理：对某些接入方开放部分接口</li>
                                            <li>限流管理：配置接口限流功能（如秒杀场景可用到）</li>
                                        </ul>

                                        <p>配置中心与接口应用分离，它们的交互方式采用Netty长连接保持通讯，并支持断开重连。交互图：</p>

                                        <p><img alt="配置中心交互图" src="https://images.gitee.com/uploads/images/2018/0728/094853_5bc4169d_332975.png" title="QQ截图20180728094824.png" /></p>
                                        <h2><a class="anchor" id="配置中心使用方法_1" href="#配置中心使用方法_1"></a>配置中心使用方法</h2><h3><a class="anchor" id="启动服务端-easyopen-config-_1" href="#启动服务端-easyopen-config-_1"></a>启动服务端（easyopen-config）</h3>
                                        <ul class="task-list">
                                            <li>导入apiconf-demo.sql脚本</li>
                                            <li>配置application.properties</li>
                                            <li>启动后访问：<a target="_blank" href="http://localhost:8070/">http://localhost:8070/</a></li>
                                            <li>用户名密码:admin 123456</li>
                                        </ul>

                                        <p><strong>注意：</strong> 建议先启动配置中心，再启动server接口应用。如果server先启动，发现配置中心没启动，会尝试加载本地配置，如果本地没有配置就启动失败。必须要正确连上配置中心一次后，下载配置文件到本地，之后启动顺序可以随意。</p>
                                        <h3><a class="anchor" id="启动前端页面-vue项目-_1" href="#启动前端页面-vue项目-_1"></a>启动前端页面（vue项目）</h3>
                                        <ul class="task-list">
                                            <li>前提：先安装好npm，<a href="https://www.cnblogs.com/ghfjj/p/6364937.html">npm安装教程</a>。建议使用淘宝镜像。</li>
                                            <li>打开src/utils/ApiUtil.js，修改url</li>
                                        </ul>

                                        <p>然后打开命令行窗口，输入下面命令</p>
                                        <div class="white"><div class="highlight"><pre><span class="c"># 初始化</span>
cnpm <span class="nb">install</span>

<span class="c"># 启动，随后出现一个url，直接复制到浏览器即可</span>
cnpm run dev
</pre></div></div><h2><a class="anchor" id="接口应用项目配置-easyopen-server-manual-_1" href="#接口应用项目配置-easyopen-server-manual-_1"></a>接口应用项目配置（easyopen-server-manual）</h2>
                                        <p>配置中心启动完毕后，需要有接口应用连接过来。</p>

                                        <ul class="task-list">
                                            <li>注意：这里的配置说明是接口应用，即demo下面的应用， <strong>不是</strong> 配置中心</li>
                                        </ul>

                                        <p>接口应用使用配置中心步骤如下：</p>

                                        <p>打开<code>IndexController.java</code></p>

                                        <ul class="task-list">
                                            <li>去掉默认的appKey，Secret配置</li>
                                        </ul>

                                        <p>去掉下面这个配置</p>

                                        <p><del>Map appSecretStore = new HashMap();</del>
                                            <del>appSecretStore.put("test", "123456");</del>
                                            <del>apiConfig.addAppSecret(appSecretStore);</del></p>

                                        <ul class="task-list">
                                            <li>去掉已有的限流配置</li>
                                        </ul>

                                        <p><del>apiConfig.setLimitManager(new ApiLimitManager(redisTemplate));</del></p>

                                        <ul class="task-list">
                                            <li>添加下面配置代码</li>
                                        </ul>
                                        <div class="white"><div class="highlight"><pre>// 配置拦截器
apiConfig.setInterceptors(new ApiInterceptor[] {
        // 限流拦截器（配置中心）
        new LimitInterceptor() ,
        // 权限拦截器（配置中心）
        new PermissionInterceptor()

        ... 其它拦截器
});

/* -----------------配置中心------------------ */
/*
 *  appName 应用名称，应用名称，不能重复
 *  host    配置中心ip，config服务器ip
 *  port    配置中心端口，对应config中的netty.server.port
 */
ConfigClient configClient = new ConfigClient("app1", "localhost", 8071);
/*
如果要使用分布式业务限流，使用下面这句。默认是单机限流
configClient.setLimitManager(new ApiLimitManager(redisTemplate, new ApiLimitConfigLocalManager()));
*/
apiConfig.setConfigClient(configClient);
/* -------------------------------------- */
</pre></div></div>
                                        <p>配置完先启动配置中心，在启动server程序</p>

                                        <hr>
                                        <h1><a class="anchor" id="客户端请求代码_1" href="#客户端请求代码_1"></a>客户端请求代码</h1><h2><a class="anchor" id="java_1" href="#java_1"></a>Java</h2><div class="white"><div class="highlight"><pre>import java.io.IOException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.junit.Test;

import com.alibaba.fastjson.JSON;

import junit.framework.TestCase;

public class PostTest extends TestCase {

    @Test
    public void testPost() throws IOException {
        String appKey = "test";
        String secret = "123456";
        // 业务参数
        Map&lt;String, String&gt; jsonMap = new HashMap&lt;String, String&gt;();
        jsonMap.put("goodsName", "iphoneX");

        String json = JSON.toJSONString(jsonMap);
        json = URLEncoder.encode(json, "utf-8");

        // 系统参数
        Map&lt;String, Object&gt; param = new HashMap&lt;String, Object&gt;();
        param.put("name", "goods.get");
        param.put("app_key", appKey);
        param.put("data", json);
        param.put("timestamp", getTime());
        param.put("version", "");

        String sign = buildSign(param, secret);

        param.put("sign", sign);

        System.out.println("=====请求数据=====");
        System.out.println(JSON.toJSON(param));
    }

    /**
     * 构建签名
     *
     * @param paramsMap
     *            参数
     * @param secret
     *            密钥
     * @return
     * @throws IOException
     */
    public static String buildSign(Map&lt;String, ?&gt; paramsMap, String secret) throws IOException {
        Set&lt;String&gt; keySet = paramsMap.keySet();
        List&lt;String&gt; paramNames = new ArrayList&lt;String&gt;(keySet);

        Collections.sort(paramNames);

        StringBuilder paramNameValue = new StringBuilder();

        for (String paramName : paramNames) {
            paramNameValue.append(paramName).append(paramsMap.get(paramName));
        }

        String source = secret + paramNameValue.toString() + secret;

        return md5(source);
    }

    /**
     * 生成md5,全部大写
     *
     * @param message
     * @return
     */
    public static String md5(String message) {
        try {
            // 1 创建一个提供信息摘要算法的对象，初始化为md5算法对象
            MessageDigest md = MessageDigest.getInstance("MD5");

            // 2 将消息变成byte数组
            byte[] input = message.getBytes();

            // 3 计算后获得字节数组,这就是那128位了
            byte[] buff = md.digest(input);

            // 4 把数组每一字节（一个字节占八位）换成16进制连成md5字符串
            return byte2hex(buff);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 二进制转十六进制字符串
     *
     * @param bytes
     * @return
     */
    private static String byte2hex(byte[] bytes) {
        StringBuilder sign = new StringBuilder();
        for (int i = 0; i &lt; bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] &amp; 0xFF);
            if (hex.length() == 1) {
                sign.append("0");
            }
            sign.append(hex.toUpperCase());
        }
        return sign.toString();
    }

    public String getTime() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
    }
}

</pre></div></div><h2><a class="anchor" id="javascript_1" href="#javascript_1"></a>JavaScript</h2><div class="white"><div class="highlight"><pre>/**
//需要发布到服务器上运行，并且server端需要处理跨域
//在IndexController.java上加@CrossOrigin(origins={"*"})

sdk.config({
    url : 'http://localhost:8080/api'
    ,app_key : 'test'
    ,secret : '123456'
    ,jwt : 'xxx'
});

sdk.post({
    name   : 'goods.get' // 接口名
//  ,version:'1.0'
//  ,access_token:''
    ,data  : {'goods_name':'iphone'} // 请求参数
    ,callback:function(resp) { // 成功回调
        console.log(resp)
    }
});

sdk.post({
    name   : 'goods.get' // 接口名
    ,data  : {'goods_name':'iphone'} // 请求参数
    ,callback:function(resp) { // 成功回调
        console.log(resp)
    }
});
 */
;(function(){

    var config = {
        url : 'http://127.0.0.1:8080/api'
        ,app_key : 'test'
        ,secret : '123456'
        ,default_version : ''

        ,api_name : "name"
        ,version_name : "version"
        ,app_key_name : "app_key"
        ,data_name : "data"
        ,timestamp_name : "timestamp"
        ,sign_name : "sign"
        ,format_name : "format"
        ,access_token_name : "access_token"
        ,jwt : ''
    }

    var DEFAULT_FORMAT = 'json';

    function copy(source, target) {
        if (target &amp;&amp; source &amp;&amp; typeof source == 'object') {
            for (var p in source) {
                target[p] = source[p];
            }
        }
        return target;
    }

    function add0(m){return m&lt;10?'0'+m:m }

    function formatDate(time)
    {
        var y = time.getFullYear();
        var m = time.getMonth()+1;
        var d = time.getDate();
        var h = time.getHours();
        var mm = time.getMinutes();
        var s = time.getSeconds();
        return y+'-'+add0(m)+'-'+add0(d)+' '+add0(h)+':'+add0(mm)+':'+add0(s);
    }

    /** 构建签名 */
    function buildSign(postData,secret) {
        var paramNames = [];
        for(var key in postData) {
            paramNames.push(key);
        }

        paramNames.sort();

        var paramNameValue = [];

        for(var i=0,len=paramNames.length;i&lt;len;i++) {
            var paramName = paramNames[i];
            paramNameValue.push(paramName);
            paramNameValue.push(postData[paramName]);
        }

        var source = secret + paramNameValue.join('') + secret;
        // MD5算法参见http://pajhome.org.uk/crypt/md5/
        return hex_md5(source).toUpperCase();
    }

    var ajax = {
        /**
         * 提交请求
         * @param options
         * { url:'',params:{},success:function(){},error:function(){} }
         */
        request:function(url,params,headers,callback,error) {
            error = error || function(e){alert('数据请求失败')};
            var xhr = this.createXhrObject();
            var paramStr = JSON.stringify(params);

            xhr.onreadystatechange = function() {
                var jsonData = '';
                if (xhr.readyState == 4){
                    var status = xhr.status;
                    if ((status &gt;= 200 &amp;&amp; status &lt; 300) || status == 304){
                        jsonData = JSON.parse(xhr.responseText);
                        callback(jsonData, paramStr);
                    } else {
                        jsonData = JSON.parse('{"message":"后台请求错误(status:' + status + ')"}');
                        console.log(xhr.responseText)
                        error(jsonData, paramStr);
                    }
                }
            };

            xhr.open('POST', url, true);
            xhr.setRequestHeader("Content-Type","application/json");
            xhr.setRequestHeader("X-Requested-With","XMLHttpRequest");
            if(headers) {
                for (var key in headers) {
                    xhr.setRequestHeader(key,headers[key]);
                }
            }

            xhr.send(paramStr);
        }
        /**
         * 创建XHR对象
         * @private
         */
        ,createXhrObject:function() {
            var methods = [
                function(){ return new XMLHttpRequest();}
                ,function(){ return new ActiveXObject('Msxml2.XMLHTTP');}
                ,function(){ return new ActiveXObject('Microsoft.XMLHTTP');}
            ];

            for(var i=0,len=methods.length; i&lt;len; i++) {
                try {
                    methods[i]();
                } catch (e) {
                    continue;
                }

                this.createXhrObject = methods[i];
                return methods[i]();
            }

            throw new Error("创建XHR对象失败");
        }
    }

    var sdk = {
        config:function(cfg) {
            copy(cfg,config);
        }
        /**
         * post请求
         * @param opts.name 接口名
         * @param opts.version 版本号
         * @param opts.data 请求数据，json对象
         * @param opts.access_token
         * @param opts.callback 响应回调
         * @param jwt jwt
         */
        ,post:function(opts, jwt) {

            var name        = opts.name,
                version     = opts.version || config.default_version,
                data        = opts.data || {},
                callback    = opts.callback,
                accessToken = opts.access_token;
            var headers = {};
            var postData = {};
            data = data || {};
            postData[config.api_name] = name;
            postData[config.version_name] = version;
            postData[config.app_key_name] = config.app_key;
            postData[config.data_name] = encodeURIComponent(JSON.stringify(data));
            postData[config.timestamp_name] = formatDate(new Date());
            postData[config.format_name] = DEFAULT_FORMAT;

            if(accessToken) {
                postData[config.access_token_name] = accessToken;
            }

            postData[config.sign_name] = buildSign(postData,config.secret);

            var _jwt = config.jwt || jwt;
            if(_jwt) {
                headers['Authorization'] = 'Bearer ' + _jwt;
            }

            ajax.request(config.url,postData,headers, callback);
        }

    }

    window.sdk = sdk;

})();
</pre></div></div><h1><a class="anchor" id="常见问题_1" href="#常见问题_1"></a>常见问题</h1><h2><a class="anchor" id="服务请求-x-超时_1" href="#服务请求-x-超时_1"></a>服务请求(x)超时</h2>
                                        <p>核对客户端时间跟服务器时间，误差不能超过3秒。调大误差变为5秒，使用<code>apiConfig.setTimeoutSeconds(5);</code></p>

                                        <p>最好是做服务器时间同步</p>
                                        <h2><a class="anchor" id="在业务方法中如何获取request对象-_1" href="#在业务方法中如何获取request对象-_1"></a>在业务方法中如何获取request对象？</h2>
                                        <p>ApiContext.getRequest()</p>
                                        <h2><a class="anchor" id="sdk加密传输json解析错误_1" href="#sdk加密传输json解析错误_1"></a>SDK加密传输json解析错误</h2>
                                        <p>如果使用了shiro等权限框架，注意配置url，IndexController里面有多个url，除了api，还有api/doc,api/ssl等，这些url都需要允许访问
                                            可以使用api*通配符来允许访问。</p>
                                        <h2><a class="anchor" id="如何获取当前调用者的appkey-_1" href="#如何获取当前调用者的appkey-_1"></a>如何获取当前调用者的appKey?</h2>
                                        <p>ApiContext.getApiParam().fatchAppKey()</p>
                                        <h2><a class="anchor" id="直接返回方法中的结果_1" href="#直接返回方法中的结果_1"></a>直接返回方法中的结果</h2><div class="white"><div class="highlight"><pre>@Api(name = "xxx"
            , wrapResult = false // 对结果不进行包装，直接将ApiResult转成json形式返回
            )
    public ApiResult fun(GoodsParam param) {
        ApiResult apiResult = new ApiResult();
        apiResult.setCode(200);
        apiResult.setMsg("xxx");
        return apiResult;
    }
</pre></div></div><h2><a class="anchor" id="拦截器中使用spring注入_1" href="#拦截器中使用spring注入_1"></a>拦截器中使用spring注入</h2>
                                        <p>// 第一步</p>
                                        <div class="white"><div class="highlight"><pre>@Component // 加这个注解
public class MyInterceptor extends ApiInterceptorAdapter {
...
}
</pre></div></div>
                                        <p>// 第二步</p>
                                        <div class="white"><div class="highlight"><pre>@Controller
@RequestMapping("/api")
public class IndexController extends ApiController {

    @Autowired
    private MyInterceptor myInterceptor;

    @Override
    protected void initApiConfig(ApiConfig apiConfig) {
        ...
        apiConfig.setInterceptors(new ApiInterceptor[]{myInterceptor});
        ...
    }

}
</pre></div></div><h1><a class="anchor" id="系统错误码_1" href="#系统错误码_1"></a>系统错误码</h1>
                                        <p>具体参见：<code>com.gitee.easyopen.message.Errors.java</code></p>

                                        <p>注：0~100为系统占用的code码</p>

                                        <ul class="task-list">
                                            <li>-9=系统错误</li>
                                            <li>1=调用不存在的服务请求</li>
                                            <li>2=服务请求参数非法</li>
                                            <li>3=服务请求缺少应用键参数</li>
                                            <li>4=服务请求的应用键参数无效</li>
                                            <li>5=服务请求需要签名,缺少签名参数</li>
                                            <li>6=服务请求的签名无效</li>
                                            <li>7=服务请求超时</li>
                                            <li>8=服务请求业务逻辑出错</li>
                                            <li>9=服务不可用</li>
                                            <li>10=服务请求时间格式有误</li>
                                            <li>11=服务请求序列化格式错误</li>
                                            <li>12=服务请求出错， contentType 不支持</li>
                                            <li>13=JSON 格式错误: </li>
                                            <li>14=accessToken错误</li>
                                            <li>15=accessToken已过期</li>
                                            <li>16=未设置accessToken</li>
                                            <li>17=操作token错误</li>
                                            <li>18=token错误</li>
                                            <li>19=算法不支持</li>
                                            <li>20=ssl交互错误</li>
                                            <li>21=jwt已过期</li>
                                        </ul>
                                    </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
<!-- ***********************************************************内容分割线****************************************************************** -->
<h1>md5.js</h1>
<a  href="static/md5.js" target="_blank">md5.js</a>
                </article>
            </div>
        </div>
    <script src="./static/jquery-1.10.2.min.js"></script>
    <script src="./static/jquery.ztree.all-3.5.min.js"></script>
    <script src="./static/jquery.ztree_toc.min.js"></script>
    <script type="text/javascript">
    var title = document.title;
        $(document).ready(function(){
            $('#tree').ztree_toc({
            	_header_nodes: [{ id:1, pId:0, name:title,open:false}],  // 第一个节点
            	ztreeSetting: {
            		view: {
        				dblClickExpand: false,
        				showLine: true,
        				showIcon: false,
        				selectedMulti: false
        			},
        			data: {
        				simpleData: {
        					enable: true,
        					idKey : "id",
        					pIdKey: "pId"
        					// rootPId: "0"
        				}
        			},
            		callback: {
        				beforeClick: function(treeId, treeNode) {
        					$('a').removeClass('curSelectedNode');
        					if(treeNode.id == 1){
        						$('body').scrollTop(0);
        					}
        					if($.fn.ztree_toc.defaults.is_highlight_selected_line == true) {
        						$('#' + treeNode.id).css('color' ,'red').fadeOut("slow" ,function() {
        							$(this).show().css('color','black');
        						});
        					}
        				}
        			}            		
            	},
                is_auto_number:true, // 菜单是否显示编号，如果markdown标题没有数字标号可以设为true
                documment_selector:'.markdown-body',
                is_expand_all: true // 菜单全部展开
            });
            // 代码高亮
            $('.highlight').each(function(i, block) {
          	    hljs.highlightBlock(block);
          	});
        });
    </script>
    </body>
</html>